/*
 * @(#)DefaultAuthHandler.java   0.3-3 06/05/2001
 *
 *  This file is part of the HTTPClient package
 *  Copyright (C) 1996-2001 Ronald Tschal�r
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free
 *  Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
 *  MA 02111-1307, USA
 *
 *  For questions, suggestions, bug-reports, enhancement-requests etc.
 *  I may be contacted at:
 *
 *  ronald@innovation.ch
 *
 *  The HTTPClient's home page is located at:
 *
 *  http://www.innovation.ch/java/HTTPClient/
 *
 */

package org.everrest.http.client;

import org.everrest.core.util.Logger;

import java.awt.BorderLayout;
import java.awt.Button;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.Label;
import java.awt.Panel;
import java.awt.TextField;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;
import java.util.Vector;

/**
 * This class is the default authorization handler. It currently handles the
 * authentication schemes "Basic", "Digest", and "SOCKS5" (used for the
 * SocksClient and not part of HTTP per se).
 * <P>
 * By default, when a username and password is required, this handler throws up
 * a message box requesting the desired info. However, applications can
 * {@link #setAuthorizationPrompter(HTTPClient.AuthorizationPrompter) set their
 * own authorization prompter} if desired.
 * <P>
 * <strong>Note:</strong> all methods except for
 * <var>setAuthorizationPrompter</var> are meant to be invoked by the
 * AuthorizationModule only, i.e. should not be invoked by the application
 * (those methods are only public because implementing the
 * <var>AuthorizationHandler</var> interface requires them to be).
 *
 * @version 0.3-3 06/05/2001
 * @author Ronald Tschal�r
 * @since V0.2
 */
public class DefaultAuthHandler implements AuthorizationHandler, GlobalConstants
{
   private static final byte[] NUL = new byte[0];

   private static final int DI_A1 = 0;

   private static final int DI_A1S = 1;

   private static final int DI_QOP = 2;

   private static byte[] digest_secret = null;

   private static AuthorizationPrompter prompter = null;

   private static boolean prompterSet = false;

   private static final Logger log = Logger.getLogger(DefaultAuthHandler.class);

   /**
    * For Digest authentication we need to set the uri, response and opaque
    * parameters. For "Basic" and "SOCKS5" nothing is done.
    */
   public AuthorizationInfo fixupAuthInfo(AuthorizationInfo info, RoRequest req, AuthorizationInfo challenge,
      RoResponse resp) throws AuthSchemeNotImplException
   {
      // nothing to do for Basic and SOCKS5 schemes

      if (info.getScheme().equalsIgnoreCase("Basic") || info.getScheme().equalsIgnoreCase("SOCKS5"))
         return info;
      else if (!info.getScheme().equalsIgnoreCase("Digest"))
         throw new AuthSchemeNotImplException(info.getScheme());

      if (log.isDebugEnabled())
         log.debug("Fixing up Authorization for host " + info.getHost() + ":" + info.getPort() + "; scheme: "
            + info.getScheme() + "; realm: " + info.getRealm());

      return digest_fixup(info, req, challenge, resp);
   }

   /**
    * returns the requested authorization, or null if none was given.
    *
    * @param challenge the parsed challenge from the server.
    * @param req the request which solicited this response
    * @param resp the full response received
    * @return a structure containing the necessary authorization info, or null
    * @exception AuthSchemeNotImplException if the authentication scheme in the
    *            challenge cannot be handled.
    */
   public AuthorizationInfo getAuthorization(AuthorizationInfo challenge, RoRequest req, RoResponse resp)
      throws AuthSchemeNotImplException, IOException
   {
      AuthorizationInfo cred;

      if (log.isDebugEnabled())
         log.debug("Requesting Authorization for host " + challenge.getHost() + ":" + challenge.getPort()
            + "; scheme: " + challenge.getScheme() + "; realm: " + challenge.getRealm());

      // we only handle Basic, Digest and SOCKS5 authentication

      if (!challenge.getScheme().equalsIgnoreCase("Basic") && !challenge.getScheme().equalsIgnoreCase("Digest")
         && !challenge.getScheme().equalsIgnoreCase("SOCKS5"))
         throw new AuthSchemeNotImplException(challenge.getScheme());

      // For digest authentication, check if stale is set

      if (challenge.getScheme().equalsIgnoreCase("Digest"))
      {
         cred = digest_check_stale(challenge, req, resp);
         if (cred != null)
            return cred;
      }

      // Ask the user for username/password

      NVPair answer;
      synchronized (getClass())
      {
         if (!req.allowUI() || prompterSet && prompter == null)
            return null;

         if (prompter == null)
            setDefaultPrompter();

         answer = prompter.getUsernamePassword(challenge, resp.getStatusCode() == 407);
      }

      if (answer == null)
         return null;

      // Now process the username/password

      if (challenge.getScheme().equalsIgnoreCase("basic"))
      {
         cred =
            new AuthorizationInfo(challenge.getHost(), challenge.getPort(), challenge.getScheme(),
               challenge.getRealm(), Codecs.base64Encode(answer.getName() + ":" + answer.getValue()));
      }
      else if (challenge.getScheme().equalsIgnoreCase("Digest"))
      {
         cred =
            digest_gen_auth_info(challenge.getHost(), challenge.getPort(), challenge.getRealm(), answer.getName(),
               answer.getValue(), req.getConnection().getContext());
         cred = digest_fixup(cred, req, challenge, null);
      }
      else
      // SOCKS5
      {
         NVPair[] upwd = {answer};
         cred =
            new AuthorizationInfo(challenge.getHost(), challenge.getPort(), challenge.getScheme(),
               challenge.getRealm(), upwd, null);
      }

      // try to get rid of any unencoded passwords in memory

      answer = null;
      //System.gc();

      // Done

      if (log.isDebugEnabled())
         log.debug("Got Authorization");

      return cred;
   }

   /**
    * We handle the "Authentication-Info" and "Proxy-Authentication-Info"
    * headers here.
    */
   public void handleAuthHeaders(Response resp, RoRequest req, AuthorizationInfo prev, AuthorizationInfo prxy)
      throws IOException
   {
      String auth_info = resp.getHeader("Authentication-Info");
      String prxy_info = resp.getHeader("Proxy-Authentication-Info");

      if (auth_info == null && prev != null && hasParam(prev.getParams(), "qop", "auth-int"))
         auth_info = "";

      if (prxy_info == null && prxy != null && hasParam(prxy.getParams(), "qop", "auth-int"))
         prxy_info = "";

      try
      {
         handleAuthInfo(auth_info, "Authentication-Info", prev, resp, req, true);
         handleAuthInfo(prxy_info, "Proxy-Authentication-Info", prxy, resp, req, true);
      }
      catch (ParseException pe)
      {
         throw new IOException(pe.toString());
      }
   }

   /**
    * We handle the "Authentication-Info" and "Proxy-Authentication-Info"
    * trailers here.
    */
   public void handleAuthTrailers(Response resp, RoRequest req, AuthorizationInfo prev, AuthorizationInfo prxy)
      throws IOException
   {
      String auth_info = resp.getTrailer("Authentication-Info");
      String prxy_info = resp.getTrailer("Proxy-Authentication-Info");

      try
      {
         handleAuthInfo(auth_info, "Authentication-Info", prev, resp, req, false);
         handleAuthInfo(prxy_info, "Proxy-Authentication-Info", prxy, resp, req, false);
      }
      catch (ParseException pe)
      {
         throw new IOException(pe.toString());
      }
   }

   private static void handleAuthInfo(String auth_info, String hdr_name, AuthorizationInfo prev, Response resp,
      RoRequest req, boolean in_headers) throws ParseException, IOException
   {
      if (auth_info == null)
         return;

      Vector pai = Util.parseHeader(auth_info);
      HttpHeaderElement elem;

      if (handle_nextnonce(prev, req, elem = Util.getElement(pai, "nextnonce")))
         pai.removeElement(elem);
      if (handle_discard(prev, req, elem = Util.getElement(pai, "discard")))
         pai.removeElement(elem);

      if (in_headers)
      {
         HttpHeaderElement qop = null;

         if (pai != null && (qop = Util.getElement(pai, "qop")) != null && qop.getValue() != null)
         {
            handle_rspauth(prev, resp, req, pai, hdr_name);
         }
         else if (prev != null
            && (Util.hasToken(resp.getHeader("Trailer"), hdr_name) && hasParam(prev.getParams(), "qop", null) || hasParam(
               prev.getParams(), "qop", "auth-int")))
         {
            handle_rspauth(prev, resp, req, null, hdr_name);
         }

         else if ((pai != null && qop == null && pai.contains(new HttpHeaderElement("digest")))
            || (Util.hasToken(resp.getHeader("Trailer"), hdr_name) && prev != null && !hasParam(prev.getParams(),
               "qop", null)))
         {
            handle_digest(prev, resp, req, hdr_name);
         }
      }

      if (pai.size() > 0)
         resp.setHeader(hdr_name, Util.assembleHeader(pai));
      else
         resp.deleteHeader(hdr_name);
   }

   private static final boolean hasParam(NVPair[] params, String name, String val)
   {
      for (int idx = 0; idx < params.length; idx++)
         if (params[idx].getName().equalsIgnoreCase(name)
            && (val == null || params[idx].getValue().equalsIgnoreCase(val)))
            return true;

      return false;
   }

   /*
    * Here are all the Digest specific methods
    */

   private static AuthorizationInfo digest_gen_auth_info(String host, int port, String realm, String user, String pass,
      Object context)
   {
      String A1 = user + ":" + realm + ":" + pass;
      String[] info = {MD5.hexDigest(A1), null, null};

      AuthorizationInfo prev = AuthorizationInfo.getAuthorization(host, port, "Digest", realm, context);
      NVPair[] params;
      if (prev == null)
      {
         params = new NVPair[4];
         params[0] = new NVPair("username", user);
         params[1] = new NVPair("uri", "");
         params[2] = new NVPair("nonce", "");
         params[3] = new NVPair("response", "");
      }
      else
      {
         params = prev.getParams();
         for (int idx = 0; idx < params.length; idx++)
         {
            if (params[idx].getName().equalsIgnoreCase("username"))
            {
               params[idx] = new NVPair("username", user);
               break;
            }
         }
      }

      return new AuthorizationInfo(host, port, "Digest", realm, params, info);
   }

   /**
    * The fixup handler
    */
   private static AuthorizationInfo digest_fixup(AuthorizationInfo info, RoRequest req, AuthorizationInfo challenge,
      RoResponse resp) throws AuthSchemeNotImplException
   {
      // get various parameters from challenge

      int ch_domain = -1, ch_nonce = -1, ch_alg = -1, ch_opaque = -1, ch_stale = -1, ch_dreq = -1, ch_qop = -1;
      NVPair[] ch_params = null;
      if (challenge != null)
      {
         ch_params = challenge.getParams();

         for (int idx = 0; idx < ch_params.length; idx++)
         {
            String name = ch_params[idx].getName().toLowerCase();
            if (name.equals("domain"))
               ch_domain = idx;
            else if (name.equals("nonce"))
               ch_nonce = idx;
            else if (name.equals("opaque"))
               ch_opaque = idx;
            else if (name.equals("algorithm"))
               ch_alg = idx;
            else if (name.equals("stale"))
               ch_stale = idx;
            else if (name.equals("digest-required"))
               ch_dreq = idx;
            else if (name.equals("qop"))
               ch_qop = idx;
         }
      }

      // get various parameters from info

      int uri = -1, user = -1, alg = -1, response = -1, nonce = -1, cnonce = -1, nc = -1, opaque = -1, digest = -1, dreq =
         -1, qop = -1;
      NVPair[] params;
      String[] extra;

      synchronized (info) // we need to juggle nonce, nc, etc
      {
         params = info.getParams();

         for (int idx = 0; idx < params.length; idx++)
         {
            String name = params[idx].getName().toLowerCase();
            if (name.equals("uri"))
               uri = idx;
            else if (name.equals("username"))
               user = idx;
            else if (name.equals("algorithm"))
               alg = idx;
            else if (name.equals("nonce"))
               nonce = idx;
            else if (name.equals("cnonce"))
               cnonce = idx;
            else if (name.equals("nc"))
               nc = idx;
            else if (name.equals("response"))
               response = idx;
            else if (name.equals("opaque"))
               opaque = idx;
            else if (name.equals("digest"))
               digest = idx;
            else if (name.equals("digest-required"))
               dreq = idx;
            else if (name.equals("qop"))
               qop = idx;
         }

         extra = (String[])info.getExtraInfo();

         // currently only MD5 hash (and "MD5-sess") is supported

         if (alg != -1 && !params[alg].getValue().equalsIgnoreCase("MD5")
            && !params[alg].getValue().equalsIgnoreCase("MD5-sess"))
            throw new AuthSchemeNotImplException("Digest auth scheme: " + "Algorithm " + params[alg].getValue()
               + " not implemented");

         if (ch_alg != -1 && !ch_params[ch_alg].getValue().equalsIgnoreCase("MD5")
            && !ch_params[ch_alg].getValue().equalsIgnoreCase("MD5-sess"))
            throw new AuthSchemeNotImplException("Digest auth scheme: " + "Algorithm " + ch_params[ch_alg].getValue()
               + " not implemented");

         // fix up uri and nonce

         params[uri] = new NVPair("uri", URI.escape(req.getRequestURI(), URI.escpdPathChar, false));
         String old_nonce = params[nonce].getValue();
         if (ch_nonce != -1 && !old_nonce.equals(ch_params[ch_nonce].getValue()))
            params[nonce] = ch_params[ch_nonce];

         // update or add optional attributes (opaque, algorithm, cnonce,
         // nonce-count, and qop

         if (ch_opaque != -1)
         {
            if (opaque == -1)
            {
               params = Util.resizeArray(params, params.length + 1);
               opaque = params.length - 1;
            }
            params[opaque] = ch_params[ch_opaque];
         }

         if (ch_alg != -1)
         {
            if (alg == -1)
            {
               params = Util.resizeArray(params, params.length + 1);
               alg = params.length - 1;
            }
            params[alg] = ch_params[ch_alg];
         }

         if (ch_qop != -1 || (ch_alg != -1 && ch_params[ch_alg].getValue().equalsIgnoreCase("MD5-sess")))
         {
            if (cnonce == -1)
            {
               params = Util.resizeArray(params, params.length + 1);
               cnonce = params.length - 1;
            }

            if (digest_secret == null)
               digest_secret = gen_random_bytes(20);

            long l_time = System.currentTimeMillis();
            byte[] time = new byte[8];
            time[0] = (byte)(l_time & 0xFF);
            time[1] = (byte)((l_time >> 8) & 0xFF);
            time[2] = (byte)((l_time >> 16) & 0xFF);
            time[3] = (byte)((l_time >> 24) & 0xFF);
            time[4] = (byte)((l_time >> 32) & 0xFF);
            time[5] = (byte)((l_time >> 40) & 0xFF);
            time[6] = (byte)((l_time >> 48) & 0xFF);
            time[7] = (byte)((l_time >> 56) & 0xFF);

            params[cnonce] = new NVPair("cnonce", MD5.hexDigest(digest_secret, time));
         }

         // select qop option

         if (ch_qop != -1)
         {
            if (qop == -1)
            {
               params = Util.resizeArray(params, params.length + 1);
               qop = params.length - 1;
            }
            extra[DI_QOP] = ch_params[ch_qop].getValue();

            // select qop option

            String[] qops = splitList(extra[DI_QOP], ",");
            String p = null;
            for (int idx = 0; idx < qops.length; idx++)
            {
               if (qops[idx].equalsIgnoreCase("auth-int")
                  && (req.getStream() == null || req.getConnection().ServProtVersKnown
                     && req.getConnection().ServerProtocolVersion >= HTTP_1_1))
               {
                  p = "auth-int";
                  break;
               }
               if (qops[idx].equalsIgnoreCase("auth"))
                  p = "auth";
            }
            if (p == null)
            {
               for (int idx = 0; idx < qops.length; idx++)
                  if (qops[idx].equalsIgnoreCase("auth-int"))
                     throw new AuthSchemeNotImplException("Digest auth scheme: Can't comply with qop "
                        + "option 'auth-int' because an HttpOutputStream "
                        + "is being used and the server doesn't speak " + "HTTP/1.1");

               throw new AuthSchemeNotImplException("Digest auth scheme: " + "None of the available qop options '"
                  + ch_params[ch_qop].getValue() + "' implemented");
            }
            params[qop] = new NVPair("qop", p);
         }

         // increment nonce-count.

         if (qop != -1)
         {
            /*
             * Note: we should actually be serializing all requests through here so
             * that the server sees the nonce-count in a strictly increasing order.
             * However, this would be amajor hassle to do, so we're just winging it.
             * Most of the time the requests will go over the wire in the same order
             * as they pass through here, but in MT apps it's possible for one
             * request to "overtake" another between here and the synchronized block
             * in sendRequest().
             */
            if (nc == -1)
            {
               params = Util.resizeArray(params, params.length + 1);
               nc = params.length - 1;
               params[nc] = new NVPair("nc", "00000001");
            }
            else if (old_nonce.equals(params[nonce].getValue()))
            {
               String c = Long.toHexString(Long.parseLong(params[nc].getValue(), 16) + 1);
               params[nc] = new NVPair("nc", "00000000".substring(c.length()) + c);
            }
            else
               params[nc] = new NVPair("nc", "00000001");
         }

         // calc new session key if necessary

         if (challenge != null && (ch_stale == -1 || !ch_params[ch_stale].getValue().equalsIgnoreCase("true"))
            && alg != -1 && params[alg].getValue().equalsIgnoreCase("MD5-sess"))
         {
            extra[DI_A1S] =
               MD5.hexDigest(extra[DI_A1] + ":" + params[nonce].getValue() + ":" + params[cnonce].getValue());
         }

         // update parameters for next auth cycle

         info.setParams(params);
         info.setExtraInfo(extra);
      }

      // calc "response" attribute

      String hash = null;
      if (qop != -1 && params[qop].getValue().equalsIgnoreCase("auth-int") && req.getStream() == null)
      {
         hash = MD5.hexDigest(req.getData() == null ? NUL : req.getData());
      }

      if (req.getStream() == null)
         params[response] =
            new NVPair("response", calcResponseAttr(hash, extra, params, alg, uri, qop, nonce, nc, cnonce, req
               .getMethod()));

      // calc digest if necessary

      AuthorizationInfo new_info;

      boolean ch_dreq_val = false;
      if (ch_dreq != -1
         && (ch_params[ch_dreq].getValue() == null || ch_params[ch_dreq].getValue().equalsIgnoreCase("true")))
         ch_dreq_val = true;

      if ((ch_dreq_val || digest != -1) && req.getStream() == null)
      {
         NVPair[] d_params;
         if (digest == -1)
         {
            d_params = Util.resizeArray(params, params.length + 1);
            digest = params.length;
         }
         else
            d_params = params;
         d_params[digest] = new NVPair("digest", calc_digest(req, extra[DI_A1], params[nonce].getValue()));

         if (dreq == -1) // if server requires digest, then so do we...
         {
            dreq = d_params.length;
            d_params = Util.resizeArray(d_params, d_params.length + 1);
            d_params[dreq] = new NVPair("digest-required", "true");
         }

         new_info =
            new AuthorizationInfo(info.getHost(), info.getPort(), info.getScheme(), info.getRealm(), d_params, extra);
      }
      else if (ch_dreq_val)
         new_info = null;
      else
         new_info =
            new AuthorizationInfo(info.getHost(), info.getPort(), info.getScheme(), info.getRealm(), params, extra);

      // add info for other domains, if listed

      boolean from_server = (challenge != null) && challenge.getHost().equalsIgnoreCase(req.getConnection().getHost());
      if (ch_domain != -1)
      {
         URI base = null;
         try
         {
            base =
               new URI(req.getConnection().getProtocol(), req.getConnection().getHost(), req.getConnection().getPort(),
                  req.getRequestURI());
         }
         catch (ParseException pe)
         {
         }

         StringTokenizer tok = new StringTokenizer(ch_params[ch_domain].getValue());
         while (tok.hasMoreTokens())
         {
            URI Uri;
            try
            {
               Uri = new URI(base, tok.nextToken());
            }
            catch (ParseException pe)
            {
               continue;
            }
            if (Uri.getHost() == null)
               continue;

            AuthorizationInfo tmp =
               AuthorizationInfo.getAuthorization(Uri.getHost(), Uri.getPort(), info.getScheme(), info.getRealm(), req
                  .getConnection().getContext());
            if (tmp == null)
            {
               params[uri] = new NVPair("uri", Uri.getPathAndQuery());
               tmp =
                  new AuthorizationInfo(Uri.getHost(), Uri.getPort(), info.getScheme(), info.getRealm(), params, extra);
               AuthorizationInfo.addAuthorization(tmp);
            }
            if (from_server)
               tmp.addPath(Uri.getPathAndQuery());
         }
      }
      else if (from_server && challenge != null)
      {
         // Spec says that if no domain attribute is present then the
         // whole server should be considered being in the same space
         AuthorizationInfo tmp =
            AuthorizationInfo.getAuthorization(challenge.getHost(), challenge.getPort(), info.getScheme(), info
               .getRealm(), req.getConnection().getContext());
         if (tmp != null)
            tmp.addPath("/");
      }

      // now return the one to use

      return new_info;
   }

   /**
    * @return the fixed info is stale=true; null otherwise
    */
   private static AuthorizationInfo digest_check_stale(AuthorizationInfo challenge, RoRequest req, RoResponse resp)
      throws AuthSchemeNotImplException, IOException
   {
      AuthorizationInfo cred = null;

      NVPair[] params = challenge.getParams();
      for (int idx = 0; idx < params.length; idx++)
      {
         String name = params[idx].getName();
         if (name.equalsIgnoreCase("stale") && params[idx].getValue().equalsIgnoreCase("true"))
         {
            cred = AuthorizationInfo.getAuthorization(challenge, req, resp, false);
            if (cred != null) // should always be the case
               return digest_fixup(cred, req, challenge, resp);
            break; // should never be reached
         }
      }

      return cred;
   }

   /**
    * Handle nextnonce field.
    */
   private static boolean handle_nextnonce(AuthorizationInfo prev, RoRequest req, HttpHeaderElement nextnonce)
      throws IOException
   {
      if (prev == null || nextnonce == null || nextnonce.getValue() == null)
         return false;

      AuthorizationInfo ai;
      try
      {
         ai = AuthorizationInfo.getAuthorization(prev, req, null, false);
      }
      catch (AuthSchemeNotImplException asnie)
      {
         ai = prev; /* shouldn't happen */
      }
      synchronized (ai)
      {
         NVPair[] params = ai.getParams();
         params = setValue(params, "nonce", nextnonce.getValue());
         params = setValue(params, "nc", "00000000");
         ai.setParams(params);
      }

      return true;
   }

   /**
    * Handle digest field of the Authentication-Info response header.
    */
   private static boolean handle_digest(AuthorizationInfo prev, Response resp, RoRequest req, String hdr_name)
      throws IOException
   {
      if (prev == null)
         return false;

      NVPair[] params = prev.getParams();
      VerifyDigest verifier =
         new VerifyDigest(((String[])prev.getExtraInfo())[0], getValue(params, "nonce"), req.getMethod(), getValue(
            params, "uri"), hdr_name, resp);

      if (resp.hasEntity())
      {
         if (log.isDebugEnabled())
            log.debug("Pushing md5-check-stream to verify digest from " + hdr_name);

         resp.inp_stream = new MD5InputStream(resp.inp_stream, verifier);
      }
      else
      {
         if (log.isDebugEnabled())
            log.debug("Verifying digest from " + hdr_name);

         verifier.verifyHash(MD5.digest(NUL), 0);
      }

      return true;
   }

   /**
    * Handle rspauth field of the Authentication-Info response header.
    */
   private static boolean handle_rspauth(AuthorizationInfo prev, Response resp, RoRequest req, Vector auth_info,
      String hdr_name) throws IOException
   {
      if (prev == null)
         return false;

      // get the parameters we sent

      NVPair[] params = prev.getParams();
      int uri = -1, alg = -1, nonce = -1, cnonce = -1, nc = -1;
      for (int idx = 0; idx < params.length; idx++)
      {
         String name = params[idx].getName().toLowerCase();
         if (name.equals("uri"))
            uri = idx;
         else if (name.equals("algorithm"))
            alg = idx;
         else if (name.equals("nonce"))
            nonce = idx;
         else if (name.equals("cnonce"))
            cnonce = idx;
         else if (name.equals("nc"))
            nc = idx;
      }

      // create hash verifier to verify rspauth

      VerifyRspAuth verifier =
         new VerifyRspAuth(params[uri].getValue(), ((String[])prev.getExtraInfo())[0], (alg == -1 ? null : params[alg]
            .getValue()), params[nonce].getValue(), (cnonce == -1 ? "" : params[cnonce].getValue()), (nc == -1 ? ""
            : params[nc].getValue()), hdr_name, resp);

      // if Authentication-Info in header and qop=auth then verify immediately

      HttpHeaderElement qop = null;
      if (auth_info != null
         && (qop = Util.getElement(auth_info, "qop")) != null
         && qop.getValue() != null
         && (qop.getValue().equalsIgnoreCase("auth") || !resp.hasEntity()
            && qop.getValue().equalsIgnoreCase("auth-int")))
      {
         if (log.isDebugEnabled())
            log.debug("Verifying rspauth from " + hdr_name);

         verifier.verifyHash(MD5.digest(NUL), 0);
      }
      else
      {
         // else push md5 stream and verify after body

         if (log.isDebugEnabled())
            log.debug("Pushing md5-check-stream to verify rspauth from " + hdr_name);

         resp.inp_stream = new MD5InputStream(resp.inp_stream, verifier);
      }

      return true;
   }

   /**
    * Calc "response" attribute for a request.
    */
   private static String calcResponseAttr(String hash, String[] extra, NVPair[] params, int alg, int uri, int qop,
      int nonce, int nc, int cnonce, String method)
   {
      String A1, A2, resp_val;

      if (alg != -1 && params[alg].getValue().equalsIgnoreCase("MD5-sess"))
         A1 = extra[DI_A1S];
      else
         A1 = extra[DI_A1];

      A2 = method + ":" + params[uri].getValue();
      if (qop != -1 && params[qop].getValue().equalsIgnoreCase("auth-int"))
      {
         A2 += ":" + hash;
      }
      A2 = MD5.hexDigest(A2);

      if (qop == -1)
         resp_val = MD5.hexDigest(A1 + ":" + params[nonce].getValue() + ":" + A2);
      else
         resp_val =
            MD5.hexDigest(A1 + ":" + params[nonce].getValue() + ":" + params[nc].getValue() + ":"
               + params[cnonce].getValue() + ":" + params[qop].getValue() + ":" + A2);

      return resp_val;
   }

   /**
    * Calculates the digest of the request body. This was in RFC-2069 and
    * draft-ietf-http-authentication-00.txt, but has subsequently been removed.
    * Here for backwards compatibility.
    */
   private static String calc_digest(RoRequest req, String A1_hash, String nonce)
   {
      if (req.getStream() != null)
         return "";

      int ct = -1, ce = -1, lm = -1, ex = -1, dt = -1;
      for (int idx = 0; idx < req.getHeaders().length; idx++)
      {
         String name = req.getHeaders()[idx].getName();
         if (name.equalsIgnoreCase("Content-type"))
            ct = idx;
         else if (name.equalsIgnoreCase("Content-Encoding"))
            ce = idx;
         else if (name.equalsIgnoreCase("Last-Modified"))
            lm = idx;
         else if (name.equalsIgnoreCase("Expires"))
            ex = idx;
         else if (name.equalsIgnoreCase("Date"))
            dt = idx;
      }

      NVPair[] hdrs = req.getHeaders();
      byte[] entity_body = (req.getData() == null ? NUL : req.getData());
      String entity_hash = MD5.hexDigest(entity_body);

      String entity_info =
         MD5.hexDigest(req.getRequestURI() + ":" + (ct == -1 ? "" : hdrs[ct].getValue()) + ":" + entity_body.length
            + ":" + (ce == -1 ? "" : hdrs[ce].getValue()) + ":" + (lm == -1 ? "" : hdrs[lm].getValue()) + ":"
            + (ex == -1 ? "" : hdrs[ex].getValue()));
      String entity_digest =
         A1_hash + ":" + nonce + ":" + req.getMethod() + ":" + (dt == -1 ? "" : hdrs[dt].getValue()) + ":"
            + entity_info + ":" + entity_hash;

      if (log.isDebugEnabled())
      {
         log.debug("Entity-Info: '" + req.getRequestURI() + ":" + (ct == -1 ? "" : hdrs[ct].getValue()) + ":"
            + entity_body.length + ":" + (ce == -1 ? "" : hdrs[ce].getValue()) + ":"
            + (lm == -1 ? "" : hdrs[lm].getValue()) + ":" + (ex == -1 ? "" : hdrs[ex].getValue()) + "'");
         log.debug("Entity-Body: '" + entity_hash + "'");
         log.debug("Entity-Digest: '" + entity_digest + "'");
      }

      return MD5.hexDigest(entity_digest);
   }

   /**
    * Handle discard token
    */
   private static boolean handle_discard(AuthorizationInfo prev, RoRequest req, HttpHeaderElement discard)
   {
      if (discard != null && prev != null)
      {
         AuthorizationInfo.removeAuthorization(prev, req.getConnection().getContext());
         return true;
      }

      return false;
   }

   /**
    * Generate <var>num</var> bytes of random data.
    *
    * @param num the number of bytes to generate
    * @return a byte array of random data
    */
   private static byte[] gen_random_bytes(int num)
   {
      // first try /dev/random
      try
      {
         FileInputStream rnd = new FileInputStream("/dev/random");
         DataInputStream din = new DataInputStream(rnd);
         byte[] data = new byte[num];
         din.readFully(data);
         try
         {
            din.close();
         }
         catch (IOException ioe)
         {
         }
         return data;
      }
      catch (Throwable t)
      {
      }

      /*
       * This is probably a much better generator, but it can be awfully slow (~ 6
       * secs / byte on my old LX)
       */
      // return new java.security.SecureRandom().getSeed(num);
      /* this is faster, but needs to be done better... */
      byte[] data = new byte[num];
      try
      {
         long fm = Runtime.getRuntime().freeMemory();
         data[0] = (byte)(fm & 0xFF);
         data[1] = (byte)((fm >> 8) & 0xFF);

         int h = data.hashCode();
         data[2] = (byte)(h & 0xFF);
         data[3] = (byte)((h >> 8) & 0xFF);
         data[4] = (byte)((h >> 16) & 0xFF);
         data[5] = (byte)((h >> 24) & 0xFF);

         long time = System.currentTimeMillis();
         data[6] = (byte)(time & 0xFF);
         data[7] = (byte)((time >> 8) & 0xFF);
      }
      catch (ArrayIndexOutOfBoundsException aioobe)
      {
      }

      return data;
   }

   /**
    * Return the value of the first NVPair whose name matches the key using a
    * case-insensitive search.
    *
    * @param list an array of NVPair's
    * @param key the key to search for
    * @return the value of the NVPair with that key, or null if not found.
    */
   private final static String getValue(NVPair[] list, String key)
   {
      int len = list.length;

      for (int idx = 0; idx < len; idx++)
         if (list[idx].getName().equalsIgnoreCase(key))
            return list[idx].getValue();

      return null;
   }

   /**
    * Return the index of the first NVPair whose name matches the key using a
    * case-insensitive search.
    *
    * @param list an array of NVPair's
    * @param key the key to search for
    * @return the index of the NVPair with that key, or -1 if not found.
    */
   private final static int getIndex(NVPair[] list, String key)
   {
      int len = list.length;

      for (int idx = 0; idx < len; idx++)
         if (list[idx].getName().equalsIgnoreCase(key))
            return idx;

      return -1;
   }

   /**
    * Sets the value of the NVPair with the name that matches the key
    * (case-insensitive). If no name matches, a new entry is created.
    *
    * @param list an array of NVPair's
    * @param key the name of the NVPair
    * @param val the value of the new NVPair
    * @return the (possibly) new list
    */
   private final static NVPair[] setValue(NVPair[] list, String key, String val)
   {
      int idx = getIndex(list, key);
      if (idx == -1)
      {
         idx = list.length;
         list = Util.resizeArray(list, list.length + 1);
      }

      list[idx] = new NVPair(key, val);
      return list;
   }

   /**
    * Split a list into an array of Strings, using sep as the separator and
    * removing whitespace around the separator.
    */
   private static String[] splitList(String str, String sep)
   {
      if (str == null)
         return new String[0];

      StringTokenizer tok = new StringTokenizer(str, sep);
      String[] list = new String[tok.countTokens()];
      for (int idx = 0; idx < list.length; idx++)
         list[idx] = tok.nextToken().trim();

      return list;
   }

   /**
    * Produce a string of the form "A5:22:F1:0B:53"
    */
   static String hex(byte[] buf)
   {
      StringBuffer str = new StringBuffer(buf.length * 3);
      for (int idx = 0; idx < buf.length; idx++)
      {
         str.append(Character.forDigit((buf[idx] >> 4) & 15, 16));
         str.append(Character.forDigit(buf[idx] & 15, 16));
         str.append(':');
      }
      str.setLength(str.length() - 1);

      return str.toString();
   }

   static final byte[] unHex(String hex)
   {
      byte[] digest = new byte[hex.length() / 2];

      for (int idx = 0; idx < digest.length; idx++)
      {
         digest[idx] = (byte)(0xFF & Integer.parseInt(hex.substring(2 * idx, 2 * (idx + 1)), 16));
      }

      return digest;
   }

   /**
    * Set a new username/password prompter.
    *
    * @param prompt the AuthorizationPrompter to use whenever a username and
    *        password are needed; if null, no querying will be done
    * @return the previous prompter
    */
   public static synchronized AuthorizationPrompter setAuthorizationPrompter(AuthorizationPrompter prompt)
   {
      AuthorizationPrompter prev = prompter;
      prompter = prompt;
      prompterSet = true;
      return prev;
   }

   /**
    * Set the default authorization prompter. It first tries to figure out if
    * the AWT is running, and if it is then the GUI popup prompter is used;
    * otherwise the command line prompter is used.
    */
   private static void setDefaultPrompter()
   {
      // if the AWT is running use the popup box; else use the
      // the command line prompter.
      if (!SimpleAuthPrompt.canUseCLPrompt() || isAWTRunning())
         prompter = new SimpleAuthPopup();
      else
         prompter = new SimpleAuthPrompt();
   }

   /**
    * Try and figure out if the AWT is running. This is done by searching all
    * threads and looking for one whose name starts with "AWT-".
    */
   private static final boolean isAWTRunning()
   {
      // find top-level thread group
      ThreadGroup root = Thread.currentThread().getThreadGroup();
      while (root.getParent() != null)
         root = root.getParent();

      // search all threads
      Thread[] t_list = new Thread[root.activeCount() + 5];
      int t_num = root.enumerate(t_list);
      for (int idx = 0; idx < t_num; idx++)
      {
         if (t_list[idx].getName().startsWith("AWT-"))
            return true;
      }

      return false;
   }
}

/**
 * This verifies the "rspauth" from draft-ietf-http-authentication-03
 */
class VerifyRspAuth implements HashVerifier, GlobalConstants
{
   private String uri;

   private String HA1;

   private String alg;

   private String nonce;

   private String cnonce;

   private String nc;

   private String hdr;

   private RoResponse resp;

   public VerifyRspAuth(String uri, String HA1, String alg, String nonce, String cnonce, String nc, String hdr,
      RoResponse resp)
   {
      this.uri = uri;
      this.HA1 = HA1;
      this.alg = alg;
      this.nonce = nonce;
      this.cnonce = cnonce;
      this.nc = nc;
      this.hdr = hdr;
      this.resp = resp;
   }

   public void verifyHash(byte[] hash, long len) throws IOException
   {
      String auth_info = resp.getHeader(hdr);
      if (auth_info == null)
         auth_info = resp.getTrailer(hdr);
      if (auth_info == null)
         return;

      Vector pai;
      try
      {
         pai = Util.parseHeader(auth_info);
      }
      catch (ParseException pe)
      {
         throw new IOException(pe.toString());
      }

      String qop;
      HttpHeaderElement elem = Util.getElement(pai, "qop");
      if (elem == null || (qop = elem.getValue()) == null
         || (!qop.equalsIgnoreCase("auth") && !qop.equalsIgnoreCase("auth-int")))
         return;

      elem = Util.getElement(pai, "rspauth");
      if (elem == null || elem.getValue() == null)
         return;
      byte[] digest = DefaultAuthHandler.unHex(elem.getValue());

      elem = Util.getElement(pai, "cnonce");
      if (elem != null && elem.getValue() != null && !elem.getValue().equals(cnonce))
         throw new IOException("Digest auth scheme: received wrong " + "client-nonce '" + elem.getValue()
            + "' - expected '" + cnonce + "'");

      elem = Util.getElement(pai, "nc");
      if (elem != null && elem.getValue() != null && !elem.getValue().equals(nc))
         throw new IOException("Digest auth scheme: received wrong " + "nonce-count '" + elem.getValue()
            + "' - expected '" + nc + "'");

      String A1, A2;
      if (alg != null && alg.equalsIgnoreCase("MD5-sess"))
         A1 = MD5.hexDigest(HA1 + ":" + nonce + ":" + cnonce);
      else
         A1 = HA1;

      // draft-01 was: A2 = resp.getStatusCode() + ":" + uri;
      A2 = ":" + uri;
      if (qop.equalsIgnoreCase("auth-int"))
         A2 += ":" + MD5.toHex(hash);
      A2 = MD5.hexDigest(A2);

      hash = MD5.digest(A1 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + A2);

      for (int idx = 0; idx < hash.length; idx++)
      {
         if (hash[idx] != digest[idx])
            throw new IOException("MD5-Digest mismatch: expected " + DefaultAuthHandler.hex(digest)
               + " but calculated " + DefaultAuthHandler.hex(hash));
      }
   }
}

/**
 * This verifies the "digest" from rfc-2069
 */
class VerifyDigest implements HashVerifier, GlobalConstants
{
   private String HA1;

   private String nonce;

   private String method;

   private String uri;

   private String hdr;

   private RoResponse resp;

   public VerifyDigest(String HA1, String nonce, String method, String uri, String hdr, RoResponse resp)
   {
      this.HA1 = HA1;
      this.nonce = nonce;
      this.method = method;
      this.uri = uri;
      this.hdr = hdr;
      this.resp = resp;
   }

   public void verifyHash(byte[] hash, long len) throws IOException
   {
      String auth_info = resp.getHeader(hdr);
      if (auth_info == null)
         auth_info = resp.getTrailer(hdr);
      if (auth_info == null)
         return;

      Vector pai;
      try
      {
         pai = Util.parseHeader(auth_info);
      }
      catch (ParseException pe)
      {
         throw new IOException(pe.toString());
      }
      HttpHeaderElement elem = Util.getElement(pai, "digest");
      if (elem == null || elem.getValue() == null)
         return;

      byte[] digest = DefaultAuthHandler.unHex(elem.getValue());

      String entity_info =
         MD5.hexDigest(uri + ":" + header_val("Content-Type", resp) + ":" + header_val("Content-Length", resp) + ":"
            + header_val("Content-Encoding", resp) + ":" + header_val("Last-Modified", resp) + ":"
            + header_val("Expires", resp));
      hash =
         MD5.digest(HA1 + ":" + nonce + ":" + method + ":" + header_val("Date", resp) + ":" + entity_info + ":"
            + MD5.toHex(hash));

      for (int idx = 0; idx < hash.length; idx++)
      {
         if (hash[idx] != digest[idx])
            throw new IOException("MD5-Digest mismatch: expected " + DefaultAuthHandler.hex(digest)
               + " but calculated " + DefaultAuthHandler.hex(hash));
      }

   }

   private static final String header_val(String hdr_name, RoResponse resp) throws IOException
   {
      String hdr = resp.getHeader(hdr_name);
      String tlr = resp.getTrailer(hdr_name);
      return (hdr != null ? hdr : (tlr != null ? tlr : ""));
   }
}

class SimpleAuthPopup implements AuthorizationPrompter
{
   private static BasicAuthBox inp = null;

   /**
    * the method called by DefaultAuthHandler.
    *
    * @return the username/password pair
    */
   public NVPair getUsernamePassword(AuthorizationInfo challenge, boolean forProxy)
   {
      String line1, line2, line3;

      if (challenge.getScheme().equalsIgnoreCase("SOCKS5"))
      {
         line1 = "Enter username and password for SOCKS server on host";
         line2 = challenge.getHost();
         line3 = "Authentication Method: username/password";
      }
      else
      {
         line1 = "Enter username and password for realm `" + challenge.getRealm() + "'";
         line2 = "on host " + challenge.getHost() + ":" + challenge.getPort();
         line3 = "Authentication Scheme: " + challenge.getScheme();
      }

      synchronized (getClass())
      {
         if (inp == null)
            inp = new BasicAuthBox();
      }

      return inp.getInput(line1, line2, line3, challenge.getScheme());
   }

   /**
    * This class implements a simple popup that request username and password
    * used for the "basic" and "digest" authentication schemes.
    *
    * @version 0.3-3 06/05/2001
    * @author Ronald Tschal�r
    */
   private static class BasicAuthBox extends Frame
   {
      private final static String title = "Authorization Request";

      private Dimension screen;

      private Label line1, line2, line3;

      private TextField user, pass;

      private int done;

      private final static int OK = 1, CANCEL = 0;

      /**
       * Constructs the popup with two lines of text above the input fields
       */
      BasicAuthBox()
      {
         super(title);

         screen = getToolkit().getScreenSize();

         addNotify();
         addWindowListener(new Close());
         setLayout(new BorderLayout());

         Panel p = new Panel(new GridLayout(3, 1));
         p.add(line1 = new Label());
         p.add(line2 = new Label());
         p.add(line3 = new Label());
         add("North", p);

         p = new Panel(new GridLayout(2, 1));
         p.add(new Label("Username:"));
         p.add(new Label("Password:"));
         add("West", p);
         p = new Panel(new GridLayout(2, 1));
         p.add(user = new TextField(30));
         p.add(pass = new TextField(30));
         pass.addActionListener(new Ok());
         pass.setEchoChar('*');
         add("East", p);

         GridBagLayout gb = new GridBagLayout();
         p = new Panel(gb);
         GridBagConstraints constr = new GridBagConstraints();
         Panel pp = new Panel();
         p.add(pp);
         constr.gridwidth = GridBagConstraints.REMAINDER;
         gb.setConstraints(pp, constr);
         constr.gridwidth = 1;
         constr.weightx = 1.0;
         Button b;
         p.add(b = new Button("  OK  "));
         b.addActionListener(new Ok());
         constr.weightx = 1.0;
         gb.setConstraints(b, constr);
         p.add(b = new Button("Clear"));
         b.addActionListener(new Clear());
         constr.weightx = 2.0;
         gb.setConstraints(b, constr);
         p.add(b = new Button("Cancel"));
         b.addActionListener(new Cancel());
         constr.weightx = 1.0;
         gb.setConstraints(b, constr);
         add("South", p);

         pack();
      }

      /**
       * our event handlers
       */
      private class Ok implements ActionListener
      {
         public void actionPerformed(ActionEvent ae)
         {
            done = OK;
            synchronized (BasicAuthBox.this)
            {
               BasicAuthBox.this.notifyAll();
            }
         }
      }

      private class Clear implements ActionListener
      {
         public void actionPerformed(ActionEvent ae)
         {
            user.setText("");
            pass.setText("");
            user.requestFocus();
         }
      }

      private class Cancel implements ActionListener
      {
         public void actionPerformed(ActionEvent ae)
         {
            done = CANCEL;
            synchronized (BasicAuthBox.this)
            {
               BasicAuthBox.this.notifyAll();
            }
         }
      }

      private class Close extends WindowAdapter
      {
         public void windowClosing(WindowEvent we)
         {
            new Cancel().actionPerformed(null);
         }
      }

      /**
       * the method called by SimpleAuthPopup.
       *
       * @return the username/password pair
       */
      synchronized NVPair getInput(String l1, String l2, String l3, String scheme)
      {
         line1.setText(l1);
         line2.setText(l2);
         line3.setText(l3);

         line1.invalidate();
         line2.invalidate();
         line3.invalidate();

         setResizable(true);
         pack();
         setResizable(false);
         setLocation((screen.width - getPreferredSize().width) / 2,
            (int)((screen.height - getPreferredSize().height) / 2 * .7));

         boolean user_focus = true;
         if (scheme.equalsIgnoreCase("NTLM"))
         {
            // prefill the user field with the username
            try
            {
               user.setText(System.getProperty("user.name", ""));
               user_focus = false;
            }
            catch (SecurityException se)
            {
            }
         }

         setVisible(true);
         if (user_focus)
            user.requestFocus();
         else
            pass.requestFocus();

         try
         {
            wait();
         }
         catch (InterruptedException e)
         {
         }

         setVisible(false);

         NVPair result = new NVPair(user.getText(), pass.getText());
         user.setText("");
         pass.setText("");

         if (done == CANCEL)
            return null;
         else
            return result;
      }
   }
}

/**
 * This class implements a simple command line prompter that request username
 * and password used for the "basic" and "digest" authentication schemes.
 *
 * @version 0.3-3 06/05/2001
 * @author Ronald Tschal�r
 */
class SimpleAuthPrompt implements AuthorizationPrompter
{
   /**
    * the method called by DefaultAuthHandler.
    *
    * @return the username/password pair
    */
   public NVPair getUsernamePassword(AuthorizationInfo challenge, boolean forProxy)
   {
      String user, pass;

      if (challenge.getScheme().equalsIgnoreCase("SOCKS5"))
      {
         System.out.println("Enter username and password for SOCKS " + "server on host " + challenge.getHost());
         System.out.println("Authentication Method: username/password");
      }
      else
      {
         System.out.println("Enter username and password for realm `" + challenge.getRealm() + "' on host "
            + challenge.getHost() + ":" + challenge.getPort());
         System.out.println("Authentication Scheme: " + challenge.getScheme());
      }

      // get username

      BufferedReader inp = new BufferedReader(new InputStreamReader(System.in));
      System.out.print("Username: ");
      System.out.flush();
      try
      {
         user = inp.readLine();
      }
      catch (IOException ioe)
      {
         return null;
      }
      if (user == null || user.length() == 0)
         return null; // cancel'd

      // get password

      echo(false);
      System.out.print("Password: ");
      System.out.flush();
      try
      {
         pass = inp.readLine();
      }
      catch (IOException ioe)
      {
         return null;
      }
      System.out.println();
      echo(true);

      if (pass == null)
         return null; // cancel'd

      // done

      return new NVPair(user, pass);
   }

   /*
    * Turn command-line echoing of typed characters on or off.
    */
   private static void echo(boolean on)
   {
      String os = System.getProperty("os.name");
      String[] cmd = null;

      if (os.equalsIgnoreCase("Windows 95") || os.equalsIgnoreCase("Windows NT"))
         // I don't think this works on M$ ...
         cmd = new String[]{"echo", on ? "on" : "off"};
      else if (os.equalsIgnoreCase("Windows") || os.equalsIgnoreCase("16-bit Windows"))
         ; // ???
      else if (os.equalsIgnoreCase("OS/2"))
         ; // ???
      else if (os.equalsIgnoreCase("Mac OS") || os.equalsIgnoreCase("MacOS"))
         ; // ???
      else if (os.equalsIgnoreCase("OpenVMS") || os.equalsIgnoreCase("VMS"))
         cmd = new String[]{"SET TERMINAL " + (on ? "/ECHO" : "/NOECHO")};
      else
         // probably unix
         cmd = new String[]{"/bin/sh", "-c", "stty " + (on ? "echo" : "-echo") + " < /dev/tty"};

      if (cmd != null)
         try
         {
            Runtime.getRuntime().exec(cmd).waitFor();
         }
         catch (Exception e)
         {
         }
   }

   /**
    * @return true for Unix's and VMS
    */
   static boolean canUseCLPrompt()
   {
      String os = System.getProperty("os.name");

      return (os.indexOf("Linux") >= 0 || os.indexOf("SunOS") >= 0 || os.indexOf("Solaris") >= 0
         || os.indexOf("BSD") >= 0 || os.indexOf("AIX") >= 0 || os.indexOf("HP-UX") >= 0 || os.indexOf("IRIX") >= 0
         || os.indexOf("OSF") >= 0 || os.indexOf("A/UX") >= 0 || os.indexOf("VMS") >= 0);
   }
}
